Skip to content

docs: lifecycle events README section [4/4]#33

Merged
choudlet merged 8 commits into
mainfrom
chrish/sc-38236/lifecycle-readme
Jul 9, 2026
Merged

docs: lifecycle events README section [4/4]#33
choudlet merged 8 commits into
mainfrom
chrish/sc-38236/lifecycle-readme

Conversation

@choudlet

Copy link
Copy Markdown
Collaborator

Shortcut: sc-38236
Parent: sc-36799
iOS reference: sc-38231
Slice 4 of 4 — README documentation. No behavior change.

Stacked on #32 (sc-38235). Merge sc-38233 + sc-38234 + sc-38235 first; this PR will retarget to main automatically.

Summary

User-facing documentation for the lifecycle-events feature shipped by slices 1–3.

  • New top-level ## Lifecycle Events section with the four-event table, cold-launch sequencing, persistence semantics, and rationale for cold-launch suppression on background-launched processes (silent push, JobScheduler, etc.).
  • Opt-in framing: trackLifecycleEvents defaults to false, sample of how to enable, note that openURL(...) is a no-op when disabled.
  • Deep-link wiring snippets for Activity.onCreate / onNewIntent and App Links via verified intent filters.
  • Privacy guidance with a URL-sanitization sample (auth tokens, OTPs, magic-link query params).
  • "Why no auto-instrumentation" rationale (no manifest mutation, no ActivityLifecycleCallbacks proxy, host control).
  • TOC entry and openURL listed in the Analytics Interface API ref.

Stack

  1. sc-38233 — storage + bundle metadata foundation
  2. sc-38234 — LifecycleEventTracker algorithm + tests
  3. sc-38235 — wiring + openURL public API
  4. this PR — README documentation

Test plan

  • README renders correctly on GitHub
  • All anchors resolve (TOC link → ## Lifecycle Events section)
  • ./gradlew :metarouter-sdk:test still passes (no behavior change)

Foundational types for the application-lifecycle events feature. Pure
additions; no behavior change to MetaRouterAnalyticsClient yet.

- LifecycleStorage: SharedPreferences wrapper for (version, build) under
  com.metarouter.analytics.lifecycle, separate from identity prefs so
  reset() does not wipe install/update state
- IdentityStorage.hasAnyValue(): identifies users that pre-date the
  lifecycle SDK (existing identity, no lifecycle keys) so the tracker
  can emit Application Updated instead of Application Installed
- IdentityManager.hasAnyValue(): forwarder
- AppContext.fromContext(): single-source-of-truth factory that reads
  PackageManager once. The lifecycle subsystem and DeviceContextProvider
  will share the cached snapshot so per-event enrichment never re-reads
  the manifest
- LifecycleEventNames / LifecycleEventProperties: extracted constants
  used by the tracker and (later) the coordinator

Refs: sc-38233
@choudlet choudlet mentioned this pull request Apr 27, 2026
5 tasks
Two small follow-ups from code review on slice 1.

- AppContext.fromContext: when the outer PackageManager read fails
  entirely, derive a best-effort `name` from the last segment of the
  package id rather than returning literal 'unknown'. Better attribution
  on ingest dashboards in the rare case the read throws (corrupted
  install / instrumentation contexts) and matches what most launchers
  show when applicationLabel resolution fails.
- AppContextTest: direct Robolectric coverage for the factory. Slice 1
  introduces fromContext standalone — it had no direct tests until now,
  only transitive coverage via DeviceContextProvider.

Refs: sc-38233
The lifecycle event tracker — install/update detection, cold-launch
sequencing, foreground/background transitions, and the one-shot
deep-link buffer. Standalone class; not yet wired into
MetaRouterAnalyticsClient (slice 3 does the wiring).

Cold-launch decision tree:
- no persisted state, no identity → Application Installed
- no persisted state, identity present → Application Updated with
  previous_version/previous_build = 'unknown' (SDK upgrade)
- persisted (version, build) matches → no install/update event
- persisted (version, build) differs → Application Updated with prior
  values

Cold-launch Opened ordering depends on the foreground-state probe:
- foreground at SDK init → emit Opened immediately and suppress the
  imminent ProcessLifecycleOwner.onStart so we don't double-emit
- background at SDK init (silent push, JobScheduler, WorkManager) →
  defer Opened to the first true ON_START transition, which then emits
  with from_background=false as the cold-launch bridge

Deep-link buffer is one-shot and last-write-wins. Multiple openURL
calls before the next Opened keep only the most recent URL; the buffer
clears once attached to an Opened payload.

The tracker has no on/off switch — when the feature is disabled the
host (LifecycleCoordinator, slice 3) simply never constructs an
instance. AppContext is injected (the cached snapshot from slice 1)
so the tracker never reads PackageManager itself.

Refs: sc-38234
Three follow-ups from code review on slice 2.

- defaultForegroundCheck: narrow the catch from Throwable to
  IllegalStateException + NoClassDefFoundError. The wide catch swallowed
  OOM / StackOverflowError silently. ProcessLifecycleOwner.get() throws
  ISE off the main thread; the lifecycle-process artifact may be
  stripped in test setups, hence NoClassDefFoundError. Anything else
  surfaces as a real bug.
- onSdkReady: AtomicBoolean idempotency guard. If the host accidentally
  invokes the cold-launch sequence twice (future re-init path, test
  misuse) we used to re-emit Installed/Updated/Opened and stomp
  suppressNextForeground, which would eat the next legitimate foreground
  transition.
- KDoc: openURL / handleDeepLink reference said EXTRA_REFERRER goes
  through getStringExtra. Per Android docs EXTRA_REFERRER is a Uri, not
  a String — getStringExtra on it is essentially always null. Point
  hosts at Activity.referrer?.host instead.
- Tests cover the new idempotency guard and the onForeground-before-
  onSdkReady contract (documenting what happens if the host wiring
  invariant breaks).

Refs: sc-38234
End-to-end integration of the lifecycle subsystem. After this slice the
feature is fully functional behind `InitOptions.trackLifecycleEvents`
— default stays false so existing customers are not affected on
upgrade.

Coordinator seam
- LifecycleCoordinator wraps LifecycleEventTracker. MetaRouterAnalytics-
  Client no longer references the tracker directly; future session /
  attribution work has a clear place to land alongside lifecycle events.
- Constructed only when trackLifecycleEvents=true. When the feature is
  off, lifecycleCoordinator is null and every dispatch site is a
  null-safe no-op.

AppContext caching
- AnalyticsClient reads AppContext.fromContext() exactly once at init
  and injects the same snapshot into both DeviceContextProvider and
  LifecycleEventTracker. The per-event enrichment path no longer hits
  PackageManager.

Public API
- handleDeepLink renamed to openURL across AnalyticsInterface,
  AnalyticsProxy, PendingCall, and the proxied call replay. Name
  matches Segment's iOS SDK and avoids the over-promise of 'handle'
  (the SDK does not route or parse the URL — it buffers it for the
  next Application Opened).
- openURL on the client logs Logger.warn and no-ops when the feature
  is disabled. Silent no-op was bad DX; misconfiguration is now
  diagnosable from logcat.

Opt-in default
- InitOptions.trackLifecycleEvents flips from true to false. KDoc
  updated. InitOptionsTest renamed to assert the new default.

Lifecycle ordering
- onBackground emits Application Backgrounded BEFORE flush /
  flushToDisk so the event lands in the same drain (unchanged behavior;
  comment kept next to the code).

Refs: sc-38235
Slice 3 follow-ups from code review.

- onForeground ordering: dispatcher.resume() now runs BEFORE
  coordinator.onForeground(). Mirrors iOS so the resumed dispatcher
  picks up the just-emitted Application Opened in its next tick rather
  than waiting for the following flush cycle. Functionally benign on
  Android (track() enqueues regardless of dispatcher state) but the
  cross-platform parity contract was an explicit constraint.
- Coordinator gate: trackLifecycleEvents is now the sole on/off signal.
  Previously a test seam injection of injectedLifecycleCoordinator could
  install a coordinator while the flag was false. Production paths were
  fine via MetaRouter.kt, but the off-state should be structurally
  enforced regardless of the seam.
- AnalyticsInterface.openURL KDoc: drop the misleading
  Intent.EXTRA_REFERRER + getStringExtra suggestion. Per Android docs
  EXTRA_REFERRER is a Uri (returns null via getStringExtra). Point hosts
  at Activity.referrer?.host, which is the canonical API for the
  calling-app host.
- DeviceContextProvider: KDoc on the appContext constructor default
  noting it exists for test ergonomics only. Production code must pass
  the explicit cached snapshot.

Refs: sc-38235
@choudlet choudlet force-pushed the chrish/sc-38235/lifecycle-wiring-openurl branch from 382bcd4 to 4e7a426 Compare April 27, 2026 21:58
Adds the user-facing documentation for the lifecycle events feature
shipped by sc-38233 / sc-38234 / sc-38235.

- Top-level Lifecycle Events section with the four events table,
  cold-launch sequencing rules, persistence semantics, and the
  rationale for cold-launch suppression on background-launched
  processes (silent push, JobScheduler, etc.)
- Opt-in framing: trackLifecycleEvents defaults to false, sample of
  how to enable, note that openURL is a no-op when disabled
- Deep-link wiring snippets for Activity.onCreate / onNewIntent and
  App Links via verified intent filters
- Privacy guidance with a URL-sanitization sample (auth tokens,
  OTPs, magic-link params)
- 'Why no auto-instrumentation' rationale (no manifest mutation, no
  ActivityLifecycleCallbacks proxy, host control)
- TOC entry and openURL listed in the Analytics Interface API ref

Refs: sc-38236
Slice 4 follow-ups from code review.

- Activity entry-point snippet: drop the misleading
  intent.getStringExtra(Intent.EXTRA_REFERRER) suggestion. EXTRA_REFERRER
  is documented as a Uri, not a String, so the String overload always
  returns null. Use Activity.referrer?.host (the canonical API) instead.
- Add an explicit imports block to the Activity snippet so it's
  copy-paste-runnable.
- Soften intent.data!! in the privacy sample to a guarded let — a
  privacy-themed example shouldn't crash on a bare bang.
- Add the android.net.Uri import to the sanitize() snippet for the same
  reason.

Refs: sc-38236
@choudlet choudlet force-pushed the chrish/sc-38236/lifecycle-readme branch from e6482b0 to fb9ad90 Compare April 27, 2026 22:00
@choudlet choudlet changed the base branch from chrish/sc-38235/lifecycle-wiring-openurl to main July 9, 2026 13:20
@choudlet choudlet merged commit f217125 into main Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant